package cn.tedu.csmall.passport.filter;

import cn.tedu.csmall.commons.consts.HttpConsts;
import cn.tedu.csmall.commons.pojo.po.AdminLoginInfoPO;
import cn.tedu.csmall.commons.security.LoginPrincipal;
import cn.tedu.csmall.commons.web.JsonResult;
import cn.tedu.csmall.commons.web.ServiceCode;
import cn.tedu.csmall.passport.repository.IAdminJwtRepository;
import com.alibaba.fastjson.JSON;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;

/**
 * <p>JWT过滤器</p>
 *
 * <p>此过滤器的主要作用</p>
 * <ul>
 *     <li>接收客户端提交的请求中的JWT</li>
 *     <li>尝试解析客户端提交的请求中的有效JWT</li>
 *     <li>将解析成功得到的数据创建为Authentication对象，并存入到SecurityContext中</li>
 * </ul>
 */
@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    @Value("${csmall.jwt.secret-key}")
    private String secretKey;

    @Autowired
    private IAdminJwtRepository adminJwtRepository;

    /**
     * JWT的最小长度值
     */
    public static final int JWT_MIN_LENGTH = 113;

    public JwtAuthorizationFilter() {
        log.debug("创建过滤器类对象：JwtAuthorizationFilter");
    }

    /**
     * 放行清单
     */
    public String[] permitUrlList = {
            "/admins/login"
    };

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.debug("JwtAuthorizationFilter开始执行...");

        // 对放行清单中的访问直接放行
        String requestURI = request.getRequestURI();
        for (String s : permitUrlList) {
            if (requestURI.equals(s)) {
                filterChain.doFilter(request, response);
                return;
            }
        }

        // 根据业内惯用的做法，客户端会将JWT放在请求头（Request Header）中的Authorization属性中
        String jwt = request.getHeader(HttpConsts.HEADER_AUTHORIZATION);
        log.debug("客户端携带的JWT：{}", jwt);

        // 判断客户端是否携带了有效的JWT
        if (!StringUtils.hasText(jwt) || jwt.length() < JWT_MIN_LENGTH) {
            // 如果JWT无效，直接放行
            log.debug("客户端没有携带有效的JWT，将放行，由后续的过滤器等组件继续处理此请求……");
            filterChain.doFilter(request, response);
            return;
        }

        // 从Redis中读取管理员登录信息
        AdminLoginInfoPO adminLoginInfoPO = adminJwtRepository.getLoginInfo(jwt);
        log.debug("从Redis中读取管理员的登录信息：{}", adminLoginInfoPO);

        // 检查是否盗用
        String remoteAddr =  request.getRemoteAddr();
        String userAgent = request.getHeader(HttpConsts.HEADER_USER_AGENT);
        if (adminLoginInfoPO == null || (!adminLoginInfoPO.getRemoteAddr().equals(remoteAddr) && !adminLoginInfoPO.getUserAgent().equals(userAgent))) {
            log.warn("JWT可能被盗用，将不向SecurityContext中存入任何认证信息，当前过滤器直接放行");
            filterChain.doFilter(request, response);
            return;
        }

        // 检查账号状态
        if (adminLoginInfoPO.getEnable() == 0) {
            log.warn("账号状态被禁用，将JWT禁用，且响应错误");
            String message = "访问错误，账号已经被禁用！";
            response.setContentType("application/json; charset=utf-8");
            PrintWriter writer = response.getWriter();
            JsonResult<Void> jsonResult = JsonResult.fail(ServiceCode.ERROR_UNAUTHORIZED_DISABLED, message);
            String jsonString = JSON.toJSONString(jsonResult);
            writer.println(jsonString);
            writer.close();
        }

        // 尝试解析JWT，并处理异常
        response.setContentType("application/json; charset=utf-8"); // 设置响应格式
        Claims claims = null;
        try {
            claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
        } catch (MalformedJwtException e) {
            // 输出错误信息的日志
            String message = "非法访问！";
            log.warn("程序运行过程中出现了MalformedJwtException，将向客户端响应错误信息！");
            log.warn("错误信息：{}", message);
            // 使用fastjson工具进行处理，将对象与JSON格式字符串相互转换
            JsonResult jsonResult = JsonResult.fail(ServiceCode.ERROR_JWT_MALFORMED, message);
            String jsonString = JSON.toJSONString(jsonResult);
            // 将响应的结果JsonResult类型转换得到的JSON格式的字符串输出
            PrintWriter printWriter = response.getWriter();
            printWriter.println(jsonString);
            printWriter.close();
            return;
        } catch (SignatureException e) {
            String message = "非法访问！";
            log.warn("程序运行过程中出现了SignatureException，将向客户端响应错误信息！");
            log.warn("错误信息：{}", message);
            JsonResult jsonResult = JsonResult.fail(ServiceCode.ERROR_JWT_SIGNATURE, message);
            String jsonString = JSON.toJSONString(jsonResult);
            PrintWriter printWriter = response.getWriter();
            printWriter.println(jsonString);
            printWriter.close();
            return;
        } catch (ExpiredJwtException e) {
            String message = "您的登录信息已经过期，请重新登录！";
            log.warn("程序运行过程中出现了ExpiredJwtException，将向客户端响应错误信息！");
            log.warn("错误信息：{}", message);
            JsonResult jsonResult = JsonResult.fail(ServiceCode.ERROR_JWT_EXPIRED, message);
            String jsonString = JSON.toJSONString(jsonResult);
            PrintWriter printWriter = response.getWriter();
            printWriter.println(jsonString);
            printWriter.close();
            return;
        } catch (Throwable e) {
            String message = "服务器忙，请稍后再试！【在开发过程中，如果看到此提示，应该检查服务器端的控制台，分析异常，并在解析JWT的过滤器中补充处理对应异常的代码块】";
            log.warn("程序运行过程中出现了Throwable，将向客户端响应错误信息！");
            log.warn("异常：", e); // 取代 e.printStackTrace();，效果相同，注意，第1个参数中不要使用 {} 进行占位
            JsonResult jsonResult = JsonResult.fail(ServiceCode.ERROR_UNKNOWN, message);
            String jsonString = JSON.toJSONString(jsonResult);
            PrintWriter printWriter = response.getWriter();
            printWriter.println(jsonString);
            printWriter.close();
            return;
        }

        // 从解析JWT得到的Claims中获取此前存入到JWT中的数据
        Long id = claims.get("id", Long.class);
        String username = claims.get("username", String.class);
        log.debug("解析JWT结束，id={}，username={}", id, username);

        // 创建当事人对象，用于存入到Authentication对象中
        LoginPrincipal loginPrincipal = new LoginPrincipal();
        loginPrincipal.setId(id);
        loginPrincipal.setUsername(username);

        // 临时处理认证信息中的权限
        List<SimpleGrantedAuthority> authorities = JSON.parseArray(adminLoginInfoPO.getAuthorityListJsonString(), SimpleGrantedAuthority.class);
        log.debug("将JWT中的权限列表的JSON字符串解析为List<GrantedAuthority>类型，结果：{}", authorities);

        // 创建认证信息，需要放当事人以及权限
        Object principal = loginPrincipal;
        Object credentials = null; // 凭证，本次不需要
        Authentication authentication = new UsernamePasswordAuthenticationToken(
                principal, credentials, authorities);
        log.debug("即将存入到SecurityContext中的Authentication对象：{}", authentication);

        // 将认证信息存入到SecurityContext中
        SecurityContext securityContext = SecurityContextHolder.getContext();
        securityContext.setAuthentication(authentication);
        log.debug("已经将Authentication对象存入到SecurityContext");

        // 放行
        log.debug("JWT过滤器执行完毕，将放行");
        filterChain.doFilter(request, response);
    }

}
